package com.cc.visitor.service;

import com.cc.alltype.BusinessException;
import com.cc.alltype.DatetimeUtils;
import com.cc.alltype.StringUtils;
import com.cc.alltype.TimeMeasurementUnit;
import com.cc.alltype.alltype.FileUtils;
import com.cc.alltype.wechat.QrType;
import com.cc.alltype.wechat.SceneQRTicketRes;
import com.cc.visitor.dao.*;
import com.cc.visitor.dto.ReqParamEnterMill;
import com.cc.visitor.entity.*;
import com.cc.visitor.enums.QrUseType;
import com.cc.visitor.enums.UserType;
import com.cc.visitor.enums.VisitProgress;
import com.cc.visitor.utils.MiniMsgUtils;
import com.cc.visitor.utils.TemplateMsgUtils;
import com.cc.visitor.utils.WechatUtils;
import com.cc.visitor.utils.WordUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.docx4j.Docx4J;
import org.docx4j.fonts.IdentityPlusMapper;
import org.docx4j.fonts.Mapper;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * @author chanchaw
 * @create 2024-09-27 10:22
 */
@Slf4j
@Service
public class VisitServiceImpl implements VisitService{
    private static final Logger logVisitor = LoggerFactory.getLogger("visitor");
    private static final String EMPTY_SIGN_IMG = "emptysign.png";
    private static final String EMPTY_SIGN_IMG_FULL = "/projs/visitor/doc/emptysign.png";
    private static final String COMPARE_DATE_PATTERN = "yyyyMMdd";
    private static final String COMPARE_DATE_HOUR_PATTERN = "yyyyMMddHH";
    private static final Logger logger = LoggerFactory.getLogger("downloadWord");
    @Value("${file.signature.word-uri}")
    private String wordUri;
    @Value("${file.signature.template-word-file}")
    private String templateWordFile;
    @Value("${file.signature.new-word-path}")
    private String newWordPath;
    @Value("${file.signature.img-path}")
    private String signatureImgPath;
    @Autowired
    private MiniMsgUtils miniMsgUtils;
    @Autowired
    private TemplateMsgUtils templateMsgUtils;
    @Autowired
    private WechatUtils wechatUtils;
    @Autowired
    private VisitService meService;
    @Autowired
    private VisitMapper dao;
    @Autowired
    private EmployeeMapper employeeMapper;
    @Autowired
    private UserWeixinMapper userWeixinMapper;
    @Autowired
    private VisitSignatureFileMapper visitSignatureFileMapper;
    @Autowired
    private LogMiniMapper logMiniMapper;

    @Override
    public List<Visit> getMyTodayThreeProgressVisit(String unionId){
        return dao.getMyTodayThreeProgressVisit(unionId);
    }

    @Transactional
    @Override
    public Visit insert(Visit record) {
        // 填充被访者的 openId 等信息
        if(record == null) {
            //logVisitor.info("新增访单时传入了空数据");
            throw new BusinessException("空数据无法保存！");
        }
        String visitorUnionId = Optional.ofNullable(record.getVisitorUnionId()).orElse("");
        if(visitorUnionId.length() == 0) {
            //logVisitor.info("新增访单的访客还没有注册：" + record.toString());
            throw new BusinessException("请先注册后再执行本操作！");
        }

        check4Insert(record);// 检测日期是否合法
        //logVisitor.info("通过调用check4Insert检测日期都合法");

        // 补全来访者信息
        UserWeixin userWeixin = userWeixinMapper.get8UnionId(visitorUnionId);
        if(userWeixin == null) {// 用户没有注册就制作访单，则后台自动新增该用户
            //logVisitor.info("访单中访客unionId=" + visitorUnionId + "还没有注册，接下来先向user_weixin中注册该用户");
            userWeixin = new UserWeixin();
            userWeixin.setUnionId(record.getVisitorUnionId());
            userWeixin.setMiniId(record.getVisitorMiniId());
            userWeixin.setOpenId(record.getVisitorOpenId());
            userWeixin.setCompany(record.getVisitorCompany());
            userWeixin.setNickName(record.getVisitorNickName());
            userWeixin.setRealName(record.getVisitorRealName());
            userWeixin.setInsertType("新增访单之前新增用户");
            String visitorPhone = Optional.ofNullable(record.getVisitorPhone()).orElse("");
            String idNumber = Optional.ofNullable(record.getVisitorIdNumber()).orElse("");
            userWeixin.setPhone(visitorPhone);
            userWeixin.setIdNumber(idNumber);
            userWeixinMapper.insertSelective(userWeixin);
            //logVisitor.info("成功注册新用户：" + userWeixin.toString());
        }
        record.setVisitorOpenId(userWeixin.getOpenId());
        record.setUserType(userWeixin.getType());

        String department = Optional.ofNullable(record.getIntervieweeDepartment()).orElse("");
        String name = Optional.ofNullable(record.getIntervieweeRealName()).orElse("");
        if(department.length() == 0) {
            //logVisitor.info("被访者部门是空，无法新增访单");
            throw new BusinessException("被访者部门不可为空！");
        }
        if(name.length() == 0) {
            //logVisitor.info("被访者姓名为空，无法新增访单！");
            throw new BusinessException("被访者姓名不可为空！");
        }

        List<Employee> employeeList = employeeMapper.getEmployee8DepartmentAndName(department, name);
        if(employeeList == null || employeeList.size() == 0) {
            //logVisitor.info("没有找到被访者员工，无法新增访单");
            throw new BusinessException("在部门[" + department + "]中没有[" + name + "]");
        }
        Employee employee = employeeList.get(0);
        record.setIntervieweeOpenId(employee.getOpenId());
        record.setIntervieweeUnionId(employee.getUnionId());
        record.setIntervieweeNickName(employee.getNickName());

        // 如果是代位审核则自动审核并不发送提醒审核的模板消息给受访者
        Integer subrogate = Optional.ofNullable(record.getSubrogate()).orElse(0);
        if(subrogate == 1){
            record.setAudit(1);
            record.setAuditTime(LocalDateTime.now());
            record.setProgress(VisitProgress.AUDITED.getSid());
            record.setSysRemark("代位审核制作的访单直接审核通过");
        }

        //logVisitor.info("即将新增访单：" + record.toString());
        int insert = dao.insert(record,true);
        Integer id = record.getId();
        //logVisitor.info("新增访单的主键：" + id);

        // 新增后修改记录写入二维码兑换券
        Visit visit = dao.selectOneById(id);
        // 2025年2月12日 14:32:28 注释下面3行代码，不再请求微信二维码兑换券
//        SceneQRTicketRes qrTicket = Optional.ofNullable(wechatUtils.getQRTicket(QrType.TEMP, meService.getQrSceneString(id))).orElse(new SceneQRTicketRes());
//        String ticketString = Optional.ofNullable(qrTicket.getTicket()).orElse("");
//        visit.setQrTicket(ticketString);

        // 推送给受访者提醒审核，点击后可跳转到对应的小程序页面
        // 代位申请的访单不需要推送提醒审核的模板消息给受访者
        if(subrogate == 0){
            templateMsgUtils.pushInterviewee2Audit(visit);
            visit.addPushInteAudit();
            dao.update(visit);
        }

        //logVisitor.info("开始设置访单相关签字记录的外键");
        meService.setChildForeignKey(visit);// 设置外键visit_signature_file.parent_id，关联到本记录主键
        //logVisitor.info("新增访单后返回给前端的数据：" + visit.toString());
        return visit;
    }

    @Override
    public int delete(Integer id) {
        return dao.deleteById(id);
    }

    @Override
    public Visit update(Visit record) {
        int update = dao.update(record);
        return dao.selectOneById(record.getId());
    }

    @Override
    public List<Visit> get8DateRange(LocalDateTime sdate, LocalDateTime edate) {
        return dao.get8DateRange(sdate, edate);
    }

    @Override
    public List<Visit> get8DateRangeOpenId(String unionId,LocalDateTime sdate, LocalDateTime edate){
        return dao.get8DateRangeOpenId(unionId, sdate, edate);
    }

    // 获取一个月的访单数据
    @Override
    public List<Visit> getMonthList8OpenId(String unionId,LocalDate date) throws Exception {
        unionId = Optional.ofNullable(unionId).orElse("");
        if(unionId.length() == 0) throw new BusinessException("没有指定要查询的微信用户，无法获取到访单数据！");
        String monthFirstDate = DatetimeUtils.getMonthFirstDate(date.toString());
        String monthLastDate = DatetimeUtils.getMonthLastDate(date.toString());
        monthFirstDate = monthFirstDate + " 00:00:00";
        monthLastDate = monthLastDate + " 00:00:00";
        LocalDateTime sdate = DatetimeUtils.string2LocalDateTime(monthFirstDate,"yyyy-MM-dd HH:mm:ss");
        LocalDateTime edate = DatetimeUtils.string2LocalDateTime(monthLastDate,"yyyy-MM-dd HH:mm:ss");
        return get8DateRangeOpenId(unionId,sdate,edate);
    }

    @Override
    public int setVoid(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        canVoid(visit);// 进场前的访单可以作废
        if(visit == null) throw new BusinessException("不存在主键" + id + "的访单！");
        visit.setState(0);

        // 设置最后一次修改人+修改时间。真实姓名优先，没有则填充昵称
        visit.setLastTime(LocalDateTime.now());
        UserWeixin userWeixinMapper8UnionId = Optional.ofNullable(userWeixinMapper.get8UnionId(visit.getVisitorUnionId())).orElse(new UserWeixin());
        String nickName = Optional.ofNullable(userWeixinMapper8UnionId.getNickName()).orElse("");
        String realName = Optional.ofNullable(userWeixinMapper8UnionId.getRealName()).orElse("");
        visit.setLastUser(realName.length() > 0 ? realName : nickName);

        int i = dao.update(visit);
        System.out.println("设置" + id + "作废的结果是：" + i);
        return i;
    }

    /**
     * 同时满足下面条件才能作废：
     * 1. 进场前的访单
     * @param record
     * @return
     */
    @Override
    public Boolean canVoid(Visit record){
        if(record == null) return false;
        String progress = Optional.ofNullable(record.getProgress()).orElse("");
        if(progress.equalsIgnoreCase(VisitProgress.ENTER.getSid()) ||
                progress.equalsIgnoreCase(VisitProgress.OUT.getSid()))
            throw new BusinessException("只有进场前的访单可以被作废！");
        return true;
    }

    /**
     * 判断一个访单是否已经被访者审核
     * @param id 访单主键 = visit.id
     * @return 返回true表示被访者已审核，相反为没有审核
     */
    @Override
    public Boolean isAudited(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        if(visit == null) throw new BusinessException("不存在的访单，无法审核！");
        String progress = Optional.ofNullable(visit.getProgress()).orElse("");
        if(progress.length() == 0) return false;
        if(progress.equalsIgnoreCase(VisitProgress.WAIT_AUDITED.getSid())) return false;
        return true;
    }

    /**
     * 被访者审核访单
     * @param id 访单主键=visit.id
     * @return
     */
    @Override
    public int audit(String unionId,Integer id){
        Boolean audited = meService.isAudited(id);
        //if(audited == true) throw new BusinessException("已审核过，无需重复操作！");
        Visit visit = dao.selectOneById(id);
        if(!visit.getIntervieweeUnionId().equalsIgnoreCase(unionId))
            throw new BusinessException("只有被访者本人可以审核本访单！");
        visit.setProgress(VisitProgress.AUDITED.getSid());
        visit.setAudit(1);
        visit.setAuditTime(LocalDateTime.now());

        int i = 0;
        // 受访者审核通过后发送访客模板消息，通知其已审核通过
        if(!meService.ignorePushVisitorAudited(id)){
            templateMsgUtils.pushVisitorAudited(visit);
            visit.addPushVisitAuditReject();
            miniMsgUtils.pushAudited(visit);
        }
        i = dao.update(visit, true);
        return i;
    }
    // 进入场馆
    @Override
    public Integer scanEnterQrCode(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        if(visit == null) throw new BusinessException("无效的二维码，只能识别本系统生成的二维码！");
        visit.setProgress(VisitProgress.ENTER.getSid());// 设置访单进度
        visit.setAdmissionTime(LocalDateTime.now());// 扫描进场的时间
        //visit.setAdmissionUnionId(unionId);// 展示进场二维码的微信用户 unionId
        //visit.setQrContent(qrContent);// 进场二维码的内容
        return dao.update(visit);
    }
    @Override
    public List<Visit> getVisit8Progress(String progress){
        return dao.getVisit8Progress(progress);
    }

    @Override
    public List<Visit> getVisit8ProgressAll(){
        return dao.getVisit8ProgressAll();
    }

    /**
     * 进行中访单，访单综合管理报表第一个标签页的数据源
     * @return
     */
    @Override
    public List<Visit> getVisitInProgress(){
        return dao.getVisitInProgress();
    }
    @Override
    public List<Visit> getVisitFinishedProgress(LocalDate sdate, LocalDate edate){
        return dao.getVisitFinishedProgress(sdate, edate);
    }
    @Override
    public VisitStatistics getVisitStatistics(@Param("unionId")String unionId){
        List<Visit> visitList = dao.getVisitStatistics(unionId);
        if(visitList == null || visitList.size() == 0) return VisitStatistics.builder().month(0).week(0).unFinished(0).unAudited(0).build();
        VisitStatistics ret = new VisitStatistics();
        ret.setMonth(visitList.size());

        Integer week = 0;Integer unFinished = 0;Integer unAudited = 0;
        for (Visit visit : visitList) {
            boolean inThisWeek = DatetimeUtils.isInThisWeek(visit.getCreateTime());
            if(inThisWeek == true) week++;
            if(!isFinished(visit)) unFinished++;
            if(!isAudited(visit)) unAudited++;
        }
        ret.setWeek(week);
        ret.setUnFinished(unFinished);
        ret.setUnAudited(unAudited);
        return ret;
    }
    public Boolean isFinished(Visit visit){
        if(visit == null) return false;
        String progress = Optional.ofNullable(visit.getProgress()).orElse("");
        Boolean isFinish = false;
        if(VisitProgress.WAIT_AUDITED.getSid().equalsIgnoreCase(progress) || VisitProgress.AUDITED.getSid().equalsIgnoreCase(progress))
            isFinish = false;
        else
            isFinish = true;
        return isFinish;
    }
    public Boolean isAudited(Visit visit){
        if(visit == null) return false;
        if(visit.getProgress().equalsIgnoreCase(VisitProgress.WAIT_AUDITED.getSid())) return false;
        return true;
    }

    /**
     * 完成返单
     * @param id 访单主键 = visit.id
     * @return
     */
    public Integer finishVisit(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        if(visit == null) return 0;
        visit.setProgress(VisitProgress.OUT.getSid());
        visit.setSysRemark("手动设置完工");
        return dao.update(visit);
    }

    /**
     * 只需要添加3个属性：id,finished,finished_union_id
     * @param record 访单对象
     * @return 返回修改数据影响的行数
     */
    @Override
    public Integer finishVisit(Visit record){
        if(record == null) throw new BusinessException("空数据无法完工！");
        Integer id = Optional.ofNullable(record.getId()).orElse(0);
        if(id == 0) throw new BusinessException("没有设置主键无法设置完工！");
        Integer finished = record.getFinished();
        if(finished == null) throw new BusinessException("请设置完工/未完工后再次尝试本操作！");
        record.setFinishedTime(LocalDateTime.now());
        return dao.update(record);
    }

    @Override
    public int audit(String unionId, Integer id, Integer audit){
        unionId = Optional.ofNullable(unionId).orElse("");
        id = Optional.ofNullable(id).orElse(0);
        audit = Optional.ofNullable(audit).orElse(0);

        Visit visit = dao.selectOneById(id);
        if(visit == null) throw new BusinessException("未找到" + id + "访单！");
        if(!visit.getIntervieweeUnionId().equalsIgnoreCase(unionId))
            throw new BusinessException("只有被访者本人可以审核本访单！");

        String progress = visit.getProgress();
        VisitProgress ofsid = VisitProgress.ofsid(progress);
        if(ofsid == VisitProgress.ENTER || ofsid == VisitProgress.OUT)
            throw new BusinessException("已进场/已完成的访单不可操作审核/驳回！");

        visit.setAudit(audit);
        visit.setAuditTime(LocalDateTime.now());
        VisitProgress visitProgress = VisitProgress.ofid(audit);
        visit.setProgress(visitProgress.getSid());
        // 审核后发送访客通知模板消息
        templateMsgUtils.pushVisitorAudited(visit);
        visit.addPushVisitAuditReject();
        int i = dao.update(visit, true);
        return i;
    }

    @Override
    public String getQrSceneString(Integer id){
        return "0," + QrUseType.VISIT.getId() + "," + id;
    }

    /**
     * 受访者驳回访单
     * @param record
     * @return
     */
    @Override
    public Integer reject(Visit record){
        if(record == null) throw new BusinessException("空数据无法驳回！");
        Integer id = Optional.ofNullable(record.getId()).orElse(0);
        String frontUnionId = Optional.ofNullable(record.getIntervieweeUnionId()).orElse("");
        String rejectRemark = Optional.ofNullable(record.getRejectRemark()).orElse("");

        Visit visit = dao.selectOneById(id);
        if(visit == null) throw new BusinessException("没有主键" + id + "的访单！");
        String dbUnionId = Optional.ofNullable(visit.getIntervieweeUnionId()).orElse("");
        if(!dbUnionId.equalsIgnoreCase(frontUnionId)) throw new BusinessException("只有受访者本人可以驳回访单！");
        visit.setRejectRemark(rejectRemark);
        visit.setAudit(-1);
        int i = dao.update(visit);

        // 推送模板消息给访客
        LocalDateTime startTime = visit.getStartTime();
        String format = startTime.format(DateTimeFormatter.ofPattern("MM-dd"));
        String visitDesc = "预约日：" + format.replace("-","月") + "日";

        String visitorOpenId = Optional.ofNullable(visit.getVisitorOpenId()).orElse("");
        // 优先推送服务号模板消息，如果访客没有注册则推送小程序订阅消息
        if(visitorOpenId.length() > 0)
            templateMsgUtils.pushVisitorReject(visit.getVisitorOpenId(),visitDesc,visit.getRejectRemark());
        else
            miniMsgUtils.pushReject(visit);
        return i;
    }
    @Override
    public Visit getLastVisit8RealName(String realName){
        return dao.getLastVisit8RealName(realName);
    }

    /**
     * 拥有特权的用户可以不输入任何信息就生成一个访单
     * @param unionId 生成本访单的微信用户unionId
     * @return
     */
    @Override
    public Visit insertNull(String unionId){
        UserWeixin userWeixin = userWeixinMapper.get8UnionId(unionId);
        if(userWeixin == null) {
            LogMini logMini = LogMini.builder().page("特权受访者新增空白信息访单").func("VisitServiceImpl#insertNull")
                    .category("java").remark("传入的unionId=" + unionId).build();
            logMiniMapper.insertSelective(logMini);
            throw new BusinessException("网络故障，请退出后重新打开本小程序！");
        }
        Visit visit = new Visit();
        visit.setIntervieweeOpenId(userWeixin.getOpenId());
        visit.setIntervieweeUnionId(unionId);
        visit.setIntervieweeNickName(userWeixin.getNickName());
        visit.setIntervieweeRealName(userWeixin.getRealName());
        visit.setIntervieweeDepartment(userWeixin.getDepartment());
        visit.setUserType(userWeixin.getType());
        visit.setStartTime(LocalDateTime.now());
        visit.setEndTime(LocalDateTime.now().plusDays(30));
        visit.setCategory("检查");
        visit.setNumberOfPeople(1);
        visit.setSysRemark("超级权限生成的没有信息的访单");
        visit.setProgress(VisitProgress.AUDITED.getSid());
        visit.setAudit(1);
        visit.setAuditTime(LocalDateTime.now());
        visit.setSubrogate(1);
        int i = dao.insertSelective(visit);

        Integer id = Optional.ofNullable(visit.getId()).orElse(0);
        SceneQRTicketRes qrTicket = Optional.ofNullable(wechatUtils.getQRTicket(QrType.TEMP,
                meService.getQrSceneString(id))).orElse(new SceneQRTicketRes());
        visit.setQrTicket(qrTicket.getTicket());
        int update = dao.update(visit);
        return visit;
    }

    /**
     * 门卫管理中扫描二维码
     * @param qrScene 二维码场景值，例如：0,4,11。依次表示：临时，门卫扫码用，访单主键=visit.id
     * @return
     */
    @Override
    public Visit scanEntryQrcode(String qrScene){
        qrScene = Optional.ofNullable(qrScene).orElse("");
        if(qrScene.length() == 0) {
            logger.info("扫描二维码的场景值：" + qrScene + "长度为0，抛出异常");
            throw new BusinessException("非法的二维码！");
        }
        String[] split = qrScene.split(",");
        if(split.length != 3) {
            logger.info("扫描二维码的场景值：" + qrScene + "长度不是3，抛出异常");
            throw new BusinessException("非法的二维码！");
        }
        Integer visitId = Optional.ofNullable(Integer.parseInt(split[2])).orElse(0);
        //Visit visit = dao.selectOneById(visitId);
        Visit visit = dao.getVisit8Id(visitId);
        if(visit == null) throw new BusinessException("不存在的访单！");
        Boolean b = check4Scan(visit);// 鉴定访单是否合法

        // 2025年3月2日 15:57:49 注释下面代码
        // 要求门卫手动点击入场，不要自动入场
        // 此处不再推送给受访者已入场的通知 - 转移到手动点击入场后再通知
//        visit.setProgress(VisitProgress.ENTER.getSid());
//        visit.setAdmissionTime(LocalDateTime.now());

        // 推送进场通知受访者 - 只推送一次
//        Integer count = Optional.ofNullable(visit.getPushInteEntry()).orElse(0);
//        if(count <= 0) {
//            templateMsgUtils.pushIntervieweeEntry(visit);
//            visit.addPushInteEntry();
//        }
//        dao.update(visit);
        return visit;
    }

    /**
     * 扫码后鉴定是否合法：访单的起始日期必须是当天
     * @param record 访单对象
     * @return false表示不合法，抛出异常提示用户
     */
    @Override
    public Boolean check4Scan(Visit record){
        // 已经作废的访单
        Integer state = record.getState();
        if(state == -1) throw new BusinessException("已作废的访单，无法进场！");
        LocalDateTime startTime = record.getStartTime();
        int i = DatetimeUtils.compareDateTo(startTime, LocalDateTime.now());
        if(i < 0) throw new BusinessException("已超时的访单，无法进场！");
        if(i > 0) throw new BusinessException("只可当天进场，您提前扫码了！");
        return true;
    }

    /**
     * 门卫处设置离场
     * @param id
     * @return
     */
    @Override
    public Integer setLeave(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        if(visit == null) throw new BusinessException("没有主键" + id + "的访单！");
        String progress = visit.getProgress();
        if(progress.equalsIgnoreCase(VisitProgress.WAIT_AUDITED.getSid()) ||
                progress.equalsIgnoreCase(VisitProgress.AUDITED.getSid()))
            throw new BusinessException("还没有进场，无法设置离场！");
        visit.setFinished(1);
        visit.setFinishedTime(LocalDateTime.now());
        visit.setFinishedUnionId("门卫");
        visit.setProgress(VisitProgress.OUT.getSid());
        templateMsgUtils.pushIntervieweeLeave(visit);
        visit.addPushInteOut();
        return dao.update(visit);
    }
    @Override
    public Visit getLastSubrogate8RealName(String realName){
        return dao.getLastSubrogate8RealName(realName);
    }
    @Override
    public Visit getLastVisit8UnionId(String unionId,Integer subrogate){
        return dao.getLastVisit8UnionId(unionId,subrogate);
    }

    /**
     * 当访客unionId和受访者unionId是同一个号码时表示
     * 受访者制作的代位申请，并且访客没有注册
     * 此时审核后不需要通知访客（因为此时访客就是受访者）
     * @param id 访单主键 = visit.id
     * @return true表示忽略，不要推送已经审核的模板消息给访客
     */
    @Override
    public Boolean ignorePushVisitorAudited(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        if(visit == null) {
            logger.info("没有主键" + id + "的代位申请");
            throw new BusinessException("没有该访单！");
        }
        String visitorUnionId = Optional.ofNullable(visit.getVisitorUnionId()).orElse("");
        String intervieweeUnionId = Optional.ofNullable(visit.getIntervieweeUnionId()).orElse("");
        return visitorUnionId.equalsIgnoreCase(intervieweeUnionId);
    }
    @Override
    public Visit selectOneById(Integer id){
        return dao.selectOneById(id);
    }

    /**
     * 设置表visit_signature_file的外键字段 parent_id = visit.id
     * @param record
     */
    @Override
    public Integer setChildForeignKey(Visit record){
        if(record == null) return 0;
        String signHashString = Optional.ofNullable(record.getSignHashString()).orElse("");
        if(signHashString.length() == 0) return 0;
        List<VisitSignatureFile> visitSignatureFileList = visitSignatureFileMapper.get8HashString(signHashString);
        if(visitSignatureFileList == null || visitSignatureFileList.size() == 0) return 0;
        VisitSignatureFile visitSignatureFile = visitSignatureFileList.get(0);
        Integer id = Optional.ofNullable(record.getId()).orElse(0);
        visitSignatureFile.setParentId(id);
        return visitSignatureFileMapper.update(visitSignatureFile);
    }

    /**
     * 检测传入的图片文件是否存在，如果不存在则返回一个默认的空白图片
     * @param imgFile 用户签名的图片文件，带有绝对路径
     * @return 生成word文件时实际使用的图片文件名，带有绝对路径
     */
    @Override
    public String getImgFile(String imgFile){
        String retFile = signatureImgPath + "spare.png";
        try {
            boolean b = FileUtils.existFile(imgFile);
            if(b) return imgFile;
        } catch (IOException e) {
            logger.info("确认签字图片" + imgFile + "是否存在时出现异常：" + e.getMessage());
        }
        logger.info("用户签字图片不存在，使用默认的" + retFile + "替代");
        return retFile;
    }
    /**
     * 导出一个访单为word文件，返回的数据模型：
     * {uri=/word/showa0010.docx, full=/projs/visitor/doc/showa0010.docx}
     * @param id 访单主键 = visit.id
     * @return 生成的新的 word 文件的绝对路径+文件名
     */
    @Override
    public Map<String,String> exportWord(Integer id) throws IOException, InvalidFormatException {
        logger.info("即将生成主键" + id + "的访单的word文件");
        id = Optional.ofNullable(id).orElse(0);
        Visit visit = dao.selectOneById(id);
        if(visit == null) {
            logger.info("没有主键" + id + "的访单，退出生成方法");
            throw new BusinessException("没有主键" + id + "的访单，无法导出！");
        }

        // 获取单据编号
        String billcode = getBillcode(id);
        String newWordFileName = billcode + ".docx";
        String newWordFileFull = newWordPath + newWordFileName;
        logger.info("即将构建的word文件：" + newWordFileName + "，文件的绝对路径是：" + newWordFileFull);

        // 获取签字图片的绝对路径文件名
        List<VisitSignatureFile> signList = visitSignatureFileMapper.get8ParentId(id);
        String imgName = "";
        String imgFileName = "";// 带有后缀名的纯文件名 - 没有路径
        if(signList != null && signList.size() > 0){
            VisitSignatureFile visitSignatureFile = signList.get(0);
            imgFileName = Optional.ofNullable(visitSignatureFile.getExtension()).orElse("");// 带有后缀名的纯文件名 - 没有路径
            imgName = signatureImgPath + imgFileName;// 带有绝对路径的文件名
            logger.info("签字的图片是：" + imgFileName + "，绝对路径：" + imgName);
            if(imgFileName.length() == 0) {
//                logger.info("没有图片文件无法生成，退出本方法");
//                throw new BusinessException("没有签字文件，无法生成文件！");
                // 2025年3月30日 14:31:33 使用空白签字图片生成签字文件
                imgFileName = EMPTY_SIGN_IMG;
                imgName = EMPTY_SIGN_IMG_FULL;
            }
        }else{
//            logger.info("没有图片文件无法生成，退出本方法");
//            throw new BusinessException("没有签字文件，无法生成文件！");
            // 2025年3月30日 14:31:33 使用空白签字图片生成签字文件
            imgFileName = EMPTY_SIGN_IMG;
            imgName = EMPTY_SIGN_IMG_FULL;
        }

        // 构建占位符数据map
        Map<String, String> map = getMap(visit);
        logger.info("根据访单数据构建的用于填充word文件的map:" + map.toString());
        String useImgFile = meService.getImgFile(imgName);
        logger.info("生成word时使用的签字图片：" + useImgFile);
        WordUtils.createWordFile(templateWordFile,newWordFileFull,map,useImgFile,imgFileName);
        logger.info("生成word文件完毕");

        HashMap<String, String> retMap = new HashMap<>();
        String uri = wordUri.substring(0,wordUri.length() - 2) + newWordFileName;// uri = /word/showa13.docx
        retMap.put("uri",uri);
        retMap.put("full",newWordFileFull);
        logger.info("即将返回给前端的map:" + retMap.toString());
        return retMap;
    }
    private Map<String, String> getMap(Visit visit){
        Map<String, String> placeholders = new HashMap<>();
        placeholders.put("${visitorName}", Optional.ofNullable(visit.getVisitorRealName()).orElse(""));
        placeholders.put("${visitorPhone}", Optional.ofNullable(visit.getVisitorPhone()).orElse(""));
        placeholders.put("${visitorIdNumber}", Optional.ofNullable(visit.getVisitorIdNumber()).orElse(""));
        placeholders.put("${visitorCompany}", Optional.ofNullable(visit.getVisitorCompany()).orElse(""));
        placeholders.put("${intervieweeDepartment}", Optional.ofNullable(visit.getIntervieweeDepartment()).orElse(""));
        placeholders.put("${intervieweeName}", Optional.ofNullable(visit.getIntervieweeRealName()).orElse(""));

        DateTimeFormatter formatDate = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String sdate = formatDate.format(Optional.ofNullable(visit.getStartTime()).orElse(LocalDateTime.now()));
        placeholders.put("${sdate}", sdate);

        DateTimeFormatter formatTime = DateTimeFormatter.ofPattern("HH:mm:ss");
        String stime = formatTime.format(Optional.ofNullable(visit.getStartTime()).orElse(LocalDateTime.now()));
        placeholders.put("${stime}", stime);

        String edate = formatDate.format(Optional.ofNullable(visit.getEndTime()).orElse(LocalDateTime.now()));
        placeholders.put("${edate}", edate);

        String etime = formatTime.format(Optional.ofNullable(visit.getEndTime()).orElse(LocalDateTime.now()));
        placeholders.put("${etime}", etime);

        placeholders.put("${peopleNumber}", visit.getNumberOfPeople().toString());
        placeholders.put("${plateNumber}", Optional.ofNullable(visit.getCarNumber()).orElse(""));
        placeholders.put("${category}", Optional.ofNullable(visit.getCategory()).orElse(""));
        placeholders.put("${remark}", Optional.ofNullable(visit.getRemark()).orElse(""));
        return placeholders;
    }
    // 根据访单主键返回单据编号
    private String getBillcode(Integer id){
        String s = StringUtils.fillToLength(id.toString(), 4, "0", true);
        return "showa" + s;
    }

    /**
     * 新增之前检测数据是否合法，返回false表示不允许新增数据
     * 1. 起始日期、终止日期必须是同一天
     * 2. 起始时间要大于等于当前时间
     * 3. 终止时间要大于起始时间
     * 4. 2025年3月19日 查总：5分钟内只能制作一个访单
     * @param record
     * @return
     */
    @Override
    public Boolean check4Insert(Visit record){
        if(record == null) throw new BusinessException("空数据无法保存！");
        LocalDateTime startTime = record.getStartTime();
        LocalDateTime endTime = record.getEndTime();

        String remark = Optional.ofNullable(record.getRemark()).orElse("");
        if(remark.length() == 0) throw new BusinessException("来访缘由是必填项！");

        // 起始日期、终止日期必须是同一天
        Boolean sameDay = DatetimeUtils.isSameDay(startTime, endTime);
        if(!sameDay) throw new BusinessException("起始日期和终止日期必须是同一天！");

        // 起始日期必须大于等于当前时间（精确到分钟）
        String s1 = DatetimeUtils.formatDate(startTime, COMPARE_DATE_PATTERN);
        String s2 = DatetimeUtils.formatDate(LocalDateTime.now(), COMPARE_DATE_PATTERN);
        long l1 = Long.parseLong(s1);
        long l2 = Long.parseLong(s2);
        if(l1 < l2) throw new BusinessException("到访起始日期必须大于等于当前日期！");


        // 终止时间必须大于起始时间（精确到分钟）  COMPARE_DATE_HOUR_PATTERN
        String s4 = DatetimeUtils.formatDate(startTime, COMPARE_DATE_HOUR_PATTERN);
        String s3 = DatetimeUtils.formatDate(endTime, COMPARE_DATE_HOUR_PATTERN);
        long l3 = Long.parseLong(s3);
        long l4 = Long.parseLong(s4);
        if(l3 < l4) throw new BusinessException("终止时间必须大于等于起始时间（精确到小时）！");

        // 5分钟内只能制作一单，配置参数在 config.sid = '连续新增访单的间隔时长'
        String visitorUnionId = Optional.ofNullable(record.getVisitorUnionId()).orElse("");
        List<Visit> visitList = dao.getVisitWithinTime(visitorUnionId);
        if(visitList != null && visitList.size() > 0) {
            // 存储过程 usp_getVisitWithinTime 中在字段 remark 构建了给用户查看的提示文案
            // 计算提示用户剩余时长
            Visit visit = visitList.get(0);
            if(visit == null) return true;
            LocalDateTime createTime = visit.getCreateTime();
            if(createTime == null) return true;
            Long durationTillNow = DatetimeUtils.getDurationTillNow(createTime, TimeMeasurementUnit.MINUTE);
            String msg = "";
            if(durationTillNow.equals(0L)){
                durationTillNow = DatetimeUtils.getDurationTillNow(createTime, TimeMeasurementUnit.SECOND);
                msg = "您已在" + durationTillNow + "秒前成功制作申请，不可重复制作！";
            }else{
                msg = "您已在" + durationTillNow + "分钟前成功制作申请，不可重复制作！";
            }
            throw new BusinessException(msg);
        }
        return true;
    }

    /**
     * 设置访单为已入场状态，扫描后请求api:visit/scanEntryQrcode
     * 其中会检测当前访单的合法性，只有合法的才会显示访单信息
     * 本方法在该逻辑之后执行，所以本方法中不鉴定访单是否合法
     * - 日期时间是否合法、是否被作废
     * @param id
     * @return
     */
    @Override
    public Integer setEntry(Integer id){
        id = Optional.ofNullable(id).orElse(0);
        if(id == 0) throw new BusinessException("没有该访单，无法入场！");
        Visit visit = dao.selectOneById(id);
        if(visit == null) throw new BusinessException("没有该访单，无法入场！");

        visit.setProgress(VisitProgress.ENTER.getSid());
        visit.setAdmissionTime(LocalDateTime.now());

        // 推送进场通知受访者 - 只推送一次
        Integer count = Optional.ofNullable(visit.getPushInteEntry()).orElse(0);
        if(count <= 0) {
            templateMsgUtils.pushIntervieweeEntry(visit);
            visit.addPushInteEntry();
        }
        return dao.update(visit);
    }

    /**
     * 获取指定受访者最近30天的访单，要求符合传入的状态查询
     * @param unionId
     * @param progress
     * @return
     */
    @Override
    public List<Visit> getVisit8ProgressRecent(String unionId,String progress){
        return dao.getVisit8ProgressRecent(unionId, progress);
    }

    /**
     * 根据主键导出为 pdf 文件，类似 exportWord 返回带有两个属性：uri,full 的 map 对象
     * @param id 访单主键 = visit.id
     * @return {uri=/word/showa0010.pdf, full=/projs/visitor/doc/showa0010.pdf}
     * @throws IOException
     * @throws InvalidFormatException
     */
    @Override
    public Map<String,String> exportPdf(Integer id) throws IOException, InvalidFormatException {
        Map<String, String> wordFileMap = meService.exportWord(id);
        logger.info("生成的word文件：" + wordFileMap.toString());
        String wordFile = wordFileMap.get("full");
        String pdfFile = word2pdfGo(wordFile);
        logger.info("word转换为pdf文件：" + pdfFile);
        String pdfFileNameWithSuffix = FileUtils.getFileNameWithSuffix(pdfFile, "/");
        String uri = wordUri.substring(0,wordUri.length() - 2) + pdfFileNameWithSuffix;// uri = /word/showa13.docx
        Map<String, String> map = new HashMap<>();
        map.put("uri", uri);
        map.put("full", pdfFile);
        logger.info("即将返回给前端的map:" + map.toString());
        return map;
    }

    private static String word2pdfGo(String docxFile){
        // 要求传入的绝对路径格式如："d:/showa/visitor001.docx"
        String pdfFile = FileUtils.changeSuffix(docxFile, "pdf", "/");
        try {
            String sourcePath = docxFile, targetPath = pdfFile;
            WordprocessingMLPackage pkg = Docx4J.load(new File(sourcePath));
            Mapper fontMapper = new IdentityPlusMapper();
            fontMapper.put("隶书", PhysicalFonts.get("LiSu"));
            fontMapper.put("宋体", PhysicalFonts.get("SimSun"));
            fontMapper.put("微软雅黑", PhysicalFonts.get("Microsoft Yahei"));
            fontMapper.put("黑体", PhysicalFonts.get("SimHei"));
            fontMapper.put("楷体", PhysicalFonts.get("KaiTi"));
            fontMapper.put("新宋体", PhysicalFonts.get("NSimSun"));
            fontMapper.put("华文行楷", PhysicalFonts.get("STXingkai"));
            fontMapper.put("华文仿宋", PhysicalFonts.get("STFangsong"));
            fontMapper.put("仿宋", PhysicalFonts.get("FangSong"));
            fontMapper.put("幼圆", PhysicalFonts.get("YouYuan"));
            fontMapper.put("华文宋体", PhysicalFonts.get("STSong"));
            fontMapper.put("华文中宋", PhysicalFonts.get("STZhongsong"));
            fontMapper.put("等线", PhysicalFonts.get("SimSun"));
            fontMapper.put("等线 Light", PhysicalFonts.get("SimSun"));
            fontMapper.put("华文琥珀", PhysicalFonts.get("STHupo"));
            fontMapper.put("华文隶书", PhysicalFonts.get("STLiti"));
            fontMapper.put("华文新魏", PhysicalFonts.get("STXinwei"));
            fontMapper.put("华文彩云", PhysicalFonts.get("STCaiyun"));
            fontMapper.put("方正姚体", PhysicalFonts.get("FZYaoti"));
            fontMapper.put("方正舒体", PhysicalFonts.get("FZShuTi"));
            fontMapper.put("华文细黑", PhysicalFonts.get("STXihei"));
            fontMapper.put("宋体扩展", PhysicalFonts.get("simsun-extB"));
            fontMapper.put("仿宋_GB2312", PhysicalFonts.get("FangSong_GB2312"));
            fontMapper.put("新細明體", PhysicalFonts.get("SimSun"));
            pkg.setFontMapper(fontMapper);
            Docx4J.toPDF(pkg, new FileOutputStream(targetPath));
            return pdfFile;
        } catch (Exception e) {
            System.out.println("转换为pdf时出现异常：" + e.getMessage());
            return e.getMessage();
        }
    }

    @Override
    public List<Visit> checkWithinTime(String visitorUnionId){
        return dao.getVisitWithinTime(visitorUnionId);
    }

    public static void main(String[] args) {
    }

}
